/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.debugger;

import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.event.LocatableEvent;

import gjexer.TExceptionDialog;

import tjide.debugger.Debugger;

/**
 * JavaScope is the Scope for a Java language process.
 */
public class JavaScope implements Scope {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(JavaScope.class.getName());

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The debugger instance.
     */
    private JavaDebugger debugger = null;

    /**
     * The location of this scope.
     */
    private Location location = null;

    /**
     * The event that generated this scope.
     */
    private LocatableEvent locatableEvent = null;

    /**
     * The thread for the event that generated this scope.
     */
    private ThreadReference thread = null;

    /**
     * The thread frame index for this scope.
     */
    private int frameIdx = 0;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Package private constructor.
     *
     * @param debugger the debugger
     * @param locatableEvent the event that generated this scope
     */
    JavaScope(final JavaDebugger debugger,
        final LocatableEvent locatableEvent) {

        this.debugger = debugger;
        this.locatableEvent = locatableEvent;
        location = locatableEvent.location();
        thread = locatableEvent.thread();
    }

    /**
     * Package private constructor.
     *
     * @param debugger the debugger
     * @param location the location in the debugged JVM process
     * @param thread the thread for the event that generated this scope
     * @param frameIdx the frame index for this location
     */
    JavaScope(final JavaDebugger debugger, final Location location,
        final ThreadReference thread, final int frameIdx) {

        this.debugger = debugger;
        this.location = location;
        this.thread = thread;
        this.frameIdx = frameIdx;
    }

    // ------------------------------------------------------------------------
    // Scope ------------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Get the source filename.
     *
     * @return the source filename
     */
    public String getSourceFilename() {
        try {
            return location.sourceName();
        } catch (AbsentInformationException e) {
            return i18n.getString("unknownSourceFilename");
        }
    }

    /**
     * Get the source line number.
     *
     * @return the line number
     */
    public int getLine() {
        return location.lineNumber();
    }

    /**
     * Get a value.
     *
     * @param expression a name that is visible within this scope.  It could
     * be local variable name or a field name.
     */
    public String getValue(final String expression) {
        assert (thread != null);

        try {
            StackFrame frame = thread.frame(frameIdx);
            LocalVariable variable = frame.visibleVariableByName(expression);
            if (variable != null) {
                // This is a local variable, return it.
                return frame.getValue(variable).toString();
            }

            // See if the expression is an object reference.
            ObjectReference thisObject = frame.thisObject();
            if (thisObject != null) {
                List<Field> fields = thisObject.referenceType().allFields();
                for (Field field: fields) {
                    if (field.name().equals(expression)) {
                        return thisObject.getValue(field).toString();
                    }
                }
            }

            // Not found in the object instance fields, check the static
            // fields.
            ReferenceType staticObject = frame.location().declaringType();
            List<Field> fields = staticObject.allFields();
            for (Field field: fields) {
                if (field.name().equals(expression)) {
                    return staticObject.getValue(field).toString();
                }
            }

            // Can't find the expression, bail out.
            return i18n.getString("unknownReference");
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just report the unknown
            // reference.
            return i18n.getString("unknownReference");
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
            return i18n.getString("unknownReference");
        }

    }

    /**
     * Get the parent scope of this scope.
     *
     * @return the scope of the calling function, or null if already at the
     * top-level scope
     */
    public Scope getParentScope() {
        assert (thread != null);

        try {
            if (thread.frameCount() == frameIdx + 1) {
                return null;
            }
            StackFrame frame = thread.frame(frameIdx + 1);
            return new JavaScope(debugger, frame.location(), thread,
                frameIdx + 1);
        } catch (final IncompatibleThreadStateException e) {
            // Bubble this up to the UI
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
            return null;
        } catch (final VMDisconnectedException e) {
            // Bubble this up to the UI
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
            return null;
        }
    }

    /**
     * Get a human-readable description of this location on the callstack.
     *
     * @return a callstack location string
     */
    public String getCallstackString() {
        assert (location != null);

        return String.format(" %s.%s(%s)",
            location.declaringType().name(),
            location.method().name(),
            location.toString());
    }

    /**
     * Get a human-readable description of every location on the callstack.
     *
     * @return a list of callstack location strings
     */
    public List<String> getAllCallstackStrings() {
        assert (thread != null);

        List<String> frames = new ArrayList<String>();
        try {
            for (int i = frameIdx; i < thread.frameCount(); i++) {
                StackFrame frame = thread.frame(i);
                frames.add(String.format(" %s.%s(%s)",
                        frame.location().declaringType().name(),
                        frame.location().method().name(),
                        frame.location().toString()));
            }
        } catch (IncompatibleThreadStateException e) {
            frames.add(i18n.getString("stackUnavailable"));
        } catch (VMDisconnectedException e) {
            frames.add(i18n.getString("stackUnavailable"));
        }
        return frames;
    }

    /**
     * Get a human-readable description of every location on the callstack
     * for a particular thread.
     *
     * @param thread the thread
     * @return a list of callstack location strings
     */
    public List<String> getAllCallstackStrings(final DebugThread thread) {
        assert (thread != null);
        assert (thread instanceof JavaThread);

        ThreadReference javaThread = ((JavaThread) thread).thread;
        List<String> frames = new ArrayList<String>();
        try {
            for (StackFrame frame: javaThread.frames()) {
                frames.add(String.format(" %s.%s(%s)",
                        frame.location().declaringType().name(),
                        frame.location().method().name(),
                        frame.location().toString()));
            }
        } catch (IncompatibleThreadStateException e) {
            frames.add(i18n.getString("stackUnavailable"));
        } catch (VMDisconnectedException e) {
            frames.add(i18n.getString("stackUnavailable"));
        }
        return frames;
    }

    /**
     * Get a list of all threads in the debugged process.
     *
     * @return list of threads
     */
    public List<DebugThread> getThreads() {
        List<DebugThread> threads = new ArrayList<DebugThread>();

        for (ThreadReference thread: location.virtualMachine().allThreads()) {
            threads.add(new JavaThread(debugger, thread));
        }

        return threads;
    }

    /**
     * Get the thread for this scope.
     *
     * @return the thread
     */
    public DebugThread getThread() {
        return new JavaThread(debugger, thread);
    }

    /**
     * Get a list of local variables in this scope.
     *
     * @return the variables
     */
    public List<DebugValue> getLocalVariables() {
        List<DebugValue> result = new ArrayList<DebugValue>();

        try {
            StackFrame frame = thread.frame(frameIdx);
            List<LocalVariable> variables = frame.visibleVariables();

            for (LocalVariable variable: variables) {
                result.add(new JavaDebugValue(variable, frame));
            }
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just return an empty list.
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
        }

        return result;
    }

    /**
     * Get a list of object ('this') variables in this scope.
     *
     * @return the variables
     */
    public List<DebugValue> getObjectVariables() {
        List<DebugValue> result = new ArrayList<DebugValue>();

        try {
            StackFrame frame = thread.frame(frameIdx);
            ObjectReference thisObject = frame.thisObject();
            if (thisObject != null) {
                ReferenceType type = thisObject.referenceType();
                List<Field> fields = type.allFields();
                for (Field field: fields) {
                    if (!field.isStatic()) {
                        result.add(new JavaDebugValue(field, thisObject));
                    }
                }
            }
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just return an empty list.
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
        }

        return result;
    }

    /**
     * Get a list of class (static) variables in this scope.
     *
     * @return the variables
     */
    public List<DebugValue> getStaticVariables() {
        List<DebugValue> result = new ArrayList<DebugValue>();

        try {
            StackFrame frame = thread.frame(frameIdx);
            ReferenceType staticObject = frame.location().declaringType();
            List<Field> fields = staticObject.allFields();

            for (Field field: fields) {
                if (field.isStatic()) {
                    result.add(new JavaDebugValue(field, staticObject));
                }
            }
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just return an empty list.
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
        }

        return result;
    }

    /**
     * Get the class name of the object type ('this') in this scope.
     *
     * @return the name
     */
    public String getObjectClassName() {
        try {
            StackFrame frame = thread.frame(frameIdx);
            ReferenceType staticObject = frame.location().declaringType();
            return staticObject.name();
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just return an unavailable
            // string.
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
        }

        return i18n.getString("unavailable");
    }

    /**
     * Get the hash code of the object type ('this') in this scope.
     *
     * @return the hash code, or 0 if there is no object in this scope
     */
    public int getObjectHashCode() {
        int result = 0;
        try {
            StackFrame frame = thread.frame(frameIdx);

            ObjectReference thisObject = frame.thisObject();
            if (thisObject != null) {
                result = thisObject.hashCode();
            }
        } catch (final VMDisconnectedException e) {
            // The debugged process is gone, just return the unavailable
            // result.
        } catch (final Exception e) {
            // Unknown exception, bubble it up.
            debugger.application.invokeLater(new Runnable() {
                public void run() {
                    new TExceptionDialog(debugger.application, e);
                }
            });
        }

        return result;
    }

}
